﻿using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;    
namespace Refraction
{
    ///<summary>
    ///Abstract camera class.
    ///Is a game component that implements IUpdateable.
    ///</summary>
    public abstract class Camera : GameComponent
    {
        #region variables
        public BoundingFrustum boundingFrustum { get; private set; }

        private bool isAppActive = true;
        public bool IsAppActive
        {
            get { return isAppActive; }
            set { isAppActive = value; }
        }

        public static Matrix view;  
        public static Matrix projection;
        public static Vector3 position;
        #endregion


        public static void SetPosition(Vector3 pos)
        {
            position = pos;
        }

        /// <summary>
        /// Initialize a new instance of this class.
        /// Abstract camera class.
        /// Is basic for each other camera class
        /// </summary>
        protected Camera(Game game)
            : base(game) { }

        /// <summary>
        /// Generate the projection matrix
        /// </summary>
        public override void Initialize()
        {
            GeneratePerspectiveProjectionMatrix(MathHelper.PiOver2);
            base.Initialize();
        }

        //Create Projection Matrix
        private void GeneratePerspectiveProjectionMatrix(float FOV)
        {
            PresentationParameters pp = Game.GraphicsDevice.PresentationParameters;
            float aspectRatio = (float)pp.BackBufferWidth / pp.BackBufferHeight;
            projection = Matrix.CreatePerspectiveFieldOfView(FOV, aspectRatio, 0.1f, 10000f);
        }

        #region Frustum Culling

        /// <summary>
        /// Generate a frustum to cull unnecessary objects
        /// </summary>
        private void GenerateFrustum()
        {
            Matrix viewProjection = view * projection;
            boundingFrustum = new BoundingFrustum(viewProjection);
        }

        /// <summary>
        /// Checks if bounding sphere is in view frustum of camera
        /// </summary>
        /// <param name="boundingSphere">Boundingsphere to check, if it is in view of camera</param>
        /// <returns>True if boundingsphere is in view of camera</returns>
        public bool IsBoundingVolumeInView(BoundingSphere boundingSphere)
        {
            return boundingFrustum.Contains(boundingSphere) != ContainmentType.Disjoint;
        }

        /// <summary>
        /// Checks if bounding box is in view frustum of camera
        /// </summary>
        /// <param name="boundingBox">Boundingbox to check, if it is in view of camera</param>
        /// <returns>True if boundingbox is in view of camera</returns>
        public bool IsBoundingVolumeInView(BoundingBox boundingBox)
        {
            return boundingFrustum.Contains(boundingBox) != ContainmentType.Disjoint;
        }
        #endregion

        /// <summary>
        /// Force Update method for subclasses
        /// </summary>
        /// <param name="gameTime">Actual time of the game</param>
        public override void Update(GameTime gameTime)
        {
        }
    }

    /// <summary>
    /// Subclass of abstract camera class.
    /// Looks at a specific point.
    /// </summary>
    public class TargetCamera : Camera
    {
        #region variables

        private Vector3 target;
        public Vector3 Target
        {
            get { return target; }
        }

        private Vector3 lookAt;
        public Vector3 LookAt
        {
            get { return lookAt; }
        }

        private Vector3 side;
        public Vector3 Side
        {
            get { return side; }
        }
        private Vector3 forward;
        public Vector3 Forward
        {
            get { return forward; }
        }

        private Vector3 up;
        public Vector3 Up
        {
            get { return Vector3.Cross(lookAt, side); }
        }

        private MouseState lastMouseState;
        private Vector2 mouseDistance;

        #endregion

        /// <summary>
        /// Initialize a new instance of this class.
        /// This Camera looks at a given target.
        /// </summary>
        /// <param name="position">Position of the camera.</param>
        /// <param name="target">Position of the target, where the camera should look at</param>
        /// <param name="game">Game</param>
        public TargetCamera(Vector3 position, Vector3 target, Game game)
            : base(game)
        {
            SetPosition(position);
            this.target = target;
            lookAt = target - position;
        }


        /// <summary>
        /// Rotates the camera
        /// </summary>
        /// <param name="yawChange">Yaw change value of camera</param>
        /// <param name="pitchChange">Pitch change value of camera</param>
        public void Rotate(float yawChange, float pitchChange)
        {
            pitchChange = MathHelper.Clamp(pitchChange, -0.2f, 0.2f);

            lookAt = Vector3.TransformNormal(lookAt, Matrix.CreateFromAxisAngle(side, pitchChange));
            lookAt = Vector3.TransformNormal(lookAt, Matrix.CreateFromAxisAngle(Vector3.Up, yawChange));

            lookAt.Normalize();
            if (Math.Abs(Vector3.Dot(Vector3.Up, lookAt)) > 0.99)
                lookAt = Vector3.TransformNormal(lookAt, Matrix.CreateFromAxisAngle(side, -pitchChange));
        }

        /// <summary>
        /// Translate the camera  of given Vector
        /// </summary>
        /// <param name="positionChange">Position change value</param>
        public void Translate(Vector3 positionChange)
        {
            position += positionChange.Z * forward;
            position += positionChange.X * side;
            position += positionChange.Y * Vector3.Up;
        }

        /// <summary>
        /// Update the camera and handles input data of mouse and keyboard.
        /// </summary>
        public override void Update(GameTime gameTime)
        {
            if (IsAppActive)
            {
                MouseState currentMouseState = Mouse.GetState();

                mouseDistance.X = currentMouseState.X - lastMouseState.X;
                mouseDistance.Y = currentMouseState.Y - lastMouseState.Y;

                //Slow mouse down
                mouseDistance *= 0.1f;

                //Initialize variables
                Vector3 translation = Vector3.Zero;
                float yaw = 0;
                float pitch = 0;

                #region Mouse Controls

                //Camera Y=Forward/Backward + X=Left/Right Rotation
                if (currentMouseState.LeftButton == ButtonState.Pressed &&
                    currentMouseState.RightButton != ButtonState.Pressed)
                {
                    translation.Z -= mouseDistance.Y;
                    yaw = -mouseDistance.X;
                }
                //Camera Rotate
                if (currentMouseState.LeftButton != ButtonState.Pressed &&
                    currentMouseState.RightButton == ButtonState.Pressed)
                {
                    pitch = -mouseDistance.Y;
                    yaw = -mouseDistance.X;
                }
                //Camera Up/Down + Left/Right
                if (currentMouseState.LeftButton == ButtonState.Pressed &&
                    currentMouseState.RightButton == ButtonState.Pressed)
                {
                    translation.Y -= mouseDistance.Y;
                    translation.X += mouseDistance.X;
                }

                #endregion

                #region Keyboard Controls

                if (Keyboard.GetState(PlayerIndex.One).IsKeyDown(Keys.W))
                {
                    translation += Vector3.Backward;
                }
                if (Keyboard.GetState(PlayerIndex.One).IsKeyDown(Keys.S))
                {
                    translation += Vector3.Forward;
                }
                if (Keyboard.GetState(PlayerIndex.One).IsKeyDown(Keys.A))
                {
                    translation += Vector3.Left;
                }
                if (Keyboard.GetState(PlayerIndex.One).IsKeyDown(Keys.D))
                {
                    translation += Vector3.Right;
                }

                #endregion

                //Relate translation/yaw/pitch to gametime, so not relies on systemspeed
                translation *= (float)gameTime.ElapsedGameTime.TotalMilliseconds * 0.05f;
                yaw *= (float)gameTime.ElapsedGameTime.TotalMilliseconds * 0.001f;
                pitch *= (float)gameTime.ElapsedGameTime.TotalMilliseconds * 0.001f;

                //Reset mouse to center, so that the mouse is not blocked by the monitor size
                Mouse.SetPosition(Game.GraphicsDevice.Viewport.Width / 2, Game.GraphicsDevice.Viewport.Height / 2);


                //Submit Manipulationdata to TargetCamera Class
                Rotate(yaw, pitch);
                Translate(translation);

                //Update MouseStates
                currentMouseState = Mouse.GetState();
                lastMouseState = currentMouseState;


                side = Vector3.Normalize(Vector3.Cross(lookAt, Vector3.Up));
                forward = Vector3.Normalize(Vector3.Cross(Vector3.Up, side));

                target = position + lookAt;
                view = Matrix.CreateLookAt(position, target, Vector3.Up);

                base.Update(gameTime);
            }
        }
    }

    //Subclass camera:chaseCamera
    public class ChaseCamera : Camera
    {

        #region Variables
        private Vector3 position;
        public Vector3 Position
        {
            get { return position; }
        }

        private Vector3 target;
        public Vector3 Target
        {
            get { return target; }
        }

        private Vector3 followTargetPosition;
        private Vector3 followTargetRotation;

        private Vector3 positionOffset;
        private Vector3 targetOffset;

        private Vector3 relativeCameraRotation;
        public Vector3 RelativeCameraRotation
        {
            get { return relativeCameraRotation; }
        }

        private float springiness = 0.15f;
        public float Springiness
        {
            set { springiness = MathHelper.Clamp(value, 0, 1); }
            get { return springiness; }
        }

        private Vector3 forward;
        public Vector3 ForwardVector
        {
            get { return forward; }
            set { forward = value; }
        }

        private Vector3 side;
        public Vector3 SideVector
        {
            get { return side; }
            set { side = value; }
        }

        private Vector3 up;
        public Vector3 Up
        {
            get { return Vector3.Cross(target, side); }
        }


        private MouseState lastMouseState;
        private Vector2 mouseDistance;

        #endregion

        //Ctor
        public ChaseCamera(Vector3 positionOffset, Vector3 targetOffset, Vector3 relativeCameraRotation, Game game)
            : base(game)
        {
            this.positionOffset = positionOffset;
            this.targetOffset = targetOffset;
            this.relativeCameraRotation = relativeCameraRotation;
        }

        public void Move(Vector3 newFollowTargetPosition, Vector3 newFollowTargetRotation)
        {
            followTargetPosition = newFollowTargetPosition;
            followTargetRotation = newFollowTargetRotation;
        }

        public void Rotate(Vector3 rotationChange)
        {
            relativeCameraRotation = rotationChange;
        }

        public override void Update(GameTime gameTime)
        {
            forward = new Vector3(target.X - position.X, 0, target.Z - position.Z);
            forward.Normalize();
            side = Vector3.Cross(forward, Vector3.Up);
            side.Normalize();

            if (IsAppActive)
            {
                #region HID Control
                MouseState currentMouseState = Mouse.GetState();

                mouseDistance.X = currentMouseState.X - lastMouseState.X;
                mouseDistance.Y = currentMouseState.Y - lastMouseState.Y;

                //Slow mouse down
                mouseDistance *= 0.1f;

                #region Mouse Controls

                //Camera Y=Forward/Backward + X=Left/Right Rotation
                if (currentMouseState.LeftButton == ButtonState.Pressed &&
                    currentMouseState.RightButton != ButtonState.Pressed)
                {
                    positionOffset -= mouseDistance.Y * forward;
                    targetOffset -= mouseDistance.Y * forward;
                    targetOffset += mouseDistance.X * side * 1.125f;
                }
                //Camera Rotate
                if (currentMouseState.LeftButton != ButtonState.Pressed &&
                    currentMouseState.RightButton == ButtonState.Pressed)
                {
                    targetOffset -= mouseDistance.Y * Vector3.Up * 1.125f;
                    targetOffset += mouseDistance.X * side * 1.125f;
                }
                //Camera Up/Down + Left/Right
                if (currentMouseState.LeftButton == ButtonState.Pressed &&
                    currentMouseState.RightButton == ButtonState.Pressed)
                {
                    positionOffset -= mouseDistance.Y * Vector3.Up;
                    targetOffset -= mouseDistance.Y * Vector3.Up;
                    positionOffset += mouseDistance.X * side;
                    targetOffset += mouseDistance.X * side;
                }

                #endregion

                #region Keyboard Controls

                if (Keyboard.GetState(PlayerIndex.One).IsKeyDown(Keys.W))
                {
                    positionOffset += forward;
                    targetOffset += forward;
                }
                if (Keyboard.GetState(PlayerIndex.One).IsKeyDown(Keys.S))
                {
                    positionOffset -= forward;
                    targetOffset -= forward;
                }
                if (Keyboard.GetState(PlayerIndex.One).IsKeyDown(Keys.A))
                {
                    positionOffset -= side;
                    targetOffset -= side;
                }
                if (Keyboard.GetState(PlayerIndex.One).IsKeyDown(Keys.D))
                {
                    positionOffset += side;
                    targetOffset += side;
                }

                #endregion


                //Mouse Controls
                //Keyboard Contols

                //Reset mouse to center, so that the mouse is not blocked by the monitor size
                Mouse.SetPosition(Game.GraphicsDevice.Viewport.Width / 2, Game.GraphicsDevice.Viewport.Height / 2);

                //Update MouseStates
                currentMouseState = Mouse.GetState();
                lastMouseState = currentMouseState;

                #endregion
            }

            //Sum the rotations of the model and the camera to ensures it
            //is rotated to the correct position telative to the models's
            //rotation
            Vector3 combinedRotation = followTargetRotation + relativeCameraRotation;

            //Calculate the rotation matrix for the camera
            Matrix rotation = Matrix.CreateFromYawPitchRoll(combinedRotation.Y, MathHelper.Clamp(combinedRotation.X, -1, 0.2f), combinedRotation.Z);

            //Calculate the position the camera would be without the spring 
            //value, using the rotation matridc and taget postion
            Vector3 desiredPostion = followTargetPosition + Vector3.Transform(positionOffset, rotation);

            //Interpolate between the current position and desired position
            position = Vector3.Lerp(position, desiredPostion, springiness);

            //Calculate the new target using the rotation matrix
            target = followTargetPosition + Vector3.Transform(targetOffset, rotation);

            //Obtain the up vector form the matrix
            Vector3 tempUp = Vector3.Transform(Vector3.Up, rotation);

            //Recalculate the view matrix
            view = Matrix.CreateLookAt(position, target, tempUp);


            base.Update(gameTime);
        }
    }
}
